using System;
using System.Diagnostics;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DaTest
{
    public class DataDrivenUnitTestHelper
    {
        private readonly object _testClassInstance;

        public DataDrivenUnitTestHelper(object testClassInstance)
        {
            _testClassInstance = testClassInstance;
        }

        public void ExerciseDataDrivenTest()
        {
            MethodBase unitTestMethod = DiscoverUnitTestMethod();
            if (unitTestMethod == null)
            {
                throw new DaTestException("Unable to find unit test method");
            }

            MethodBase dataTestMethod = DiscoverDataTestMethod(unitTestMethod);
            if (dataTestMethod == null)
            {
                throw new DaTestException("Unable to find corresponding data-driven test method");
            }

            Exercise(_testClassInstance, dataTestMethod);
        }

        private static void Exercise(object testClassInstance, MethodBase dataTestMethod)
        {
            var parametersAttributes =
                (ParametersAttribute[])Attribute.GetCustomAttributes(dataTestMethod, typeof(ParametersAttribute));
            
            foreach (var parametersAttribute in parametersAttributes)
            {
                var convertedParameterValues = ConvertParameterValues(parametersAttribute.Parameters, dataTestMethod.GetParameters());
                dataTestMethod.Invoke(testClassInstance, convertedParameterValues);
            }
        }

        private static object[] ConvertParameterValues(object[] inputData, ParameterInfo[] parameterInfos)
        {
            var result = new object[inputData.Length];
            for (var i = 0; i < inputData.Length; i++)
            {
                var parameterType = parameterInfos[i].ParameterType;
                try
                {
                    result[i] = Convert.ChangeType(inputData[i], parameterType);
                }
                catch (InvalidCastException)
                {
                    result[i] = GetOperator(parameterType).Invoke(null, new[] { inputData[i] });
                }
            }
            return result;
        }

        private static MethodInfo GetOperator(Type t)
        {
            return t.GetMethod("op_Implicit") ?? t.GetMethod("op_Explicit");
        }

        private static MethodBase DiscoverDataTestMethod(MethodBase unitTestMethod)
        {
            foreach (var method in unitTestMethod.DeclaringType.GetMethods())
            {
                if (IsDataTestMethod(method, unitTestMethod))
                {
                    return method;
                }
            }
            return null;
        }

        private static bool IsDataTestMethod(MemberInfo candidateMethod, MethodBase unitTestMethod)
        {
            return
                Attribute.IsDefined(candidateMethod, typeof (ParametersAttribute)) &&
                candidateMethod.Name.Equals(unitTestMethod.Name);
        }

        private static MethodBase DiscoverUnitTestMethod()
        {
            foreach (var frame in new StackTrace().GetFrames())
            {
                var method = frame.GetMethod();
                if (Attribute.IsDefined(method, typeof(TestMethodAttribute)))
                {
                    return method;
                }
            }
            return null;
        }
    }
}